﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Oridea.Data.Fetching {
	/// <summary>
	/// Represents a sequence of Expression&lt;T, TKey&gt; that can be applied to an IQueryable&lt;T&gt;
	/// so as to order its rows in the defined manner.
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public class OrderingSequence<T> : IEnumerable<Expression> {
		public class OrderByExpression {
			public Expression Expression { get; set; }
			public Type ObjectType { get; set; }
			public Type KeyType { get; set; }
		}

		private static readonly MethodInfo QueryableOrderBy;
		private static readonly MethodInfo QueryableThenBy;

		static OrderingSequence() {
			// This is surely possible to optimize, written quickly, though already works fast enough
			MethodInfo[] queryableMethods = typeof (Queryable).GetMethods();
			QueryableOrderBy = queryableMethods.Single(m => m.Name == "OrderBy" && m.GetParameters().Length == 2);
			QueryableThenBy = queryableMethods.Single(m => m.Name == "ThenBy" && m.GetParameters().Length == 2);
		}

		private readonly List<OrderByExpression> _orderByExpressions = new List<OrderByExpression>();

		/// <summary>
		/// Adds an ordering expression to the sequence.
		/// </summary>
		public void Add<TKey>(Expression<Func<T, TKey>> keySelector) {
			if (keySelector == null) {
				throw new ArgumentNullException("keySelector");
			}
			_orderByExpressions.Add(new OrderByExpression {
				Expression = keySelector,
				ObjectType = typeof(T),
				KeyType = typeof(TKey)
			});
		}

		/// <summary>
		/// Applies ordering sequence to the IQueryable&lt;T&gt; by appropriately calling OrderBy and ThenBy
		/// static methods of Queryable static class.
		/// </summary>
		public IQueryable<T> ApplyTo(IQueryable<T> query) {
			if (query == null) {
				throw new ArgumentNullException("query");
			}

			for (int i = 0; i < _orderByExpressions.Count; i++) {
				OrderByExpression orderByExpression = _orderByExpressions[i];
				query = CallOrderingMethodForExpression(query, orderByExpression, 
					i == 0 ? QueryableOrderBy : QueryableThenBy);
			}
			return query;
		}

		private static IQueryable<T> CallOrderingMethodForExpression(IQueryable<T> query, OrderByExpression orderByExpression, MethodInfo genericMethodBase) {
			MethodInfo genericOrderBy = 
				genericMethodBase.MakeGenericMethod(orderByExpression.ObjectType, orderByExpression.KeyType);
			query = (IOrderedQueryable<T>) genericOrderBy.Invoke(null, new object[] {
				query, orderByExpression.Expression
			});
			return query;
		}

		public IEnumerator<Expression> GetEnumerator() {
			return GetGenericEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator() {
			return GetGenericEnumerator();
		}

		private IEnumerator<Expression> GetGenericEnumerator() {
			return _orderByExpressions.Select(o => o.Expression).GetEnumerator();
		}
	}
}
